#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace Osh.Teletext
{
	public struct DisplayPageAddress : IComparable, IComparable<DisplayPageAddress>, IEquatable<DisplayPageAddress>
	{
		public static DisplayPageAddress Empty;

		public const int NullPageNumber = 0xFF;

		private const int MinMagazineNumber = 1;
		private const int MaxMagazineNumber = 8;
		private const int MaxPageNumber = 0xFF;
		private const int MaxSubCodeNumber = 0x3F7F;
		private const int NotEmptyFlag = 0x80;

		private byte _magazine;
		private byte _pageNumber;
		private ushort _subCode;

		public DisplayPageAddress(PageAddress address)
		{
			_magazine = (byte)((address.Magazine == 0 ? 8 : address.Magazine) | NotEmptyFlag);
			_pageNumber = (byte)address.PageNumber;
			_subCode = (ushort)address.SubCode;
		}

		public DisplayPageAddress(string pageAddress)
		{
			DisplayPageAddress address;
			if(!TryParse(pageAddress, out address))
				throw new ArgumentOutOfRangeException("pageAddress");

			_magazine = address._magazine;
			_pageNumber = address._pageNumber;
			_subCode = address._subCode;
		}

		public DisplayPageAddress(int magazineAndPageNumber, int subCode)
			: this(magazineAndPageNumber >> 8, magazineAndPageNumber & 0xFF, subCode)
		{
		}

		public DisplayPageAddress(int magazine, int pageNumber, int subCode)
		{
			if(magazine < MinMagazineNumber || magazine > MaxMagazineNumber)
				throw new ArgumentOutOfRangeException("magazine");
			if(pageNumber < 0 || pageNumber > MaxPageNumber)
				throw new ArgumentOutOfRangeException("pageNumber");
			if(subCode < 0 || subCode > MaxSubCodeNumber)
				throw new ArgumentOutOfRangeException("subCode");

			_magazine = (byte)(magazine | NotEmptyFlag);
			_pageNumber = (byte)pageNumber;
			_subCode = (ushort)subCode;
		}

		public int Magazine
		{
			get { return _magazine & ~NotEmptyFlag; }
			set
			{
				if(value < MinMagazineNumber || value > MaxMagazineNumber)
					throw new ArgumentOutOfRangeException("Magazine");
				_magazine = (byte)(value | NotEmptyFlag);
			}
		}

		public int PageNumber
		{
			get { return _pageNumber; }
			set
			{
				if(value < 0 || value > MaxPageNumber)
					throw new ArgumentOutOfRangeException("PageNumber");
				_pageNumber = (byte)value;
			}
		}

		public int MagazineAndPageNumber
		{
			get { return (this.Magazine << 8) | _pageNumber; }
			set
			{
				byte magazineNumber = (byte)(value >> 8);
				byte pageNumber = (byte)(value & 0xFF);

				if(magazineNumber < MinMagazineNumber || magazineNumber > MaxMagazineNumber)
					throw new ArgumentOutOfRangeException("MagazineAndPageNumber");
				if(pageNumber < 0 || pageNumber > MaxPageNumber)
					throw new ArgumentOutOfRangeException("MagazineAndPageNumber");

				this.Magazine = magazineNumber;
				_pageNumber = pageNumber;
			}
		}

		public int SubCode
		{
			get { return _subCode; }
			set
			{
				if(value < 0 || value > MaxSubCodeNumber)
					throw new ArgumentOutOfRangeException("SubCode");
				_subCode = (ushort)value;
			}
		}

		public bool IsCompatible(DisplayPageAddress address)
		{
			if(this == address)
				return true;
			
			if(this.MagazineAndPageNumber == address.MagazineAndPageNumber &&
				(this.SubCode == PageAddress.NullSubCodeNumber || address.SubCode == PageAddress.NullSubCodeNumber))
				return true;
			
			return false;
		}

		private uint Value
		{
			get { return (uint)((_magazine << 24) | (_pageNumber << 16) | _subCode); }
		}

		public override string ToString()
		{
			return this.Magazine.ToString("X") +
				_pageNumber.ToString("X").PadLeft(2, '0') + "." +
				_subCode.ToString("X").PadLeft(4, '0');
		}

		public DisplayPageAddress ChangeMagazineAndPageNumber(int delta)
		{
			int newMagazineAndPageNumber = ChangeNumber(this.MagazineAndPageNumber, delta, 100, 899);
			DisplayPageAddress result = this;
			result.MagazineAndPageNumber = newMagazineAndPageNumber;
			return result;
		}

		public DisplayPageAddress ChangeSubCode(int delta)
		{
			int subCode = ChangeNumber(this.SubCode, delta, 1, 9999);
			DisplayPageAddress result = this;
			result.SubCode = subCode;
			return result;
		}

		private int ChangeNumber(int hexNumber, int delta, int minValue, int maxValue)
		{
			int num = ToDecimal(hexNumber) + delta;
			if(num < minValue) num = minValue;
			if(num > maxValue) num = maxValue;
			return ToHex(num);
		}

		/// <summary>
		/// Converts hexadecimal number (e.g. 0x1234) into a similar looking decimal number (1234).
		/// If hex number contains hex letters (A-F) they will be rounded to the maximum decimal digit (9).
		/// For example hexadecimal number 0x12F0 will be converted to 1290.
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		public static int ToDecimal(int value)
		{
			if(value < 0 || value > 0xFFFF)
				throw new ArgumentOutOfRangeException("value");

			int digit0 = Math.Min(value % 16, 9);
			int digit1 = Math.Min((value >> 4) % 16, 9);
			int digit2 = Math.Min((value >> 8) % 16, 9);
			int digit3 = Math.Min((value >> 12) % 16, 9);
			return (digit3 * 1000) + (digit2 * 100) + (digit1 * 10) + digit0;
		}

		/// <summary>
		/// Converts decimal number (e.g. 1234) into a similar looking hexadecimal number (0x1234).
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		public static int ToHex(int value)
		{
			if(value < 0 || value > 0xFFFF)
				throw new ArgumentOutOfRangeException("value");

			int digit0 = value % 10;
			int digit1 = (value / 10) % 10;
			int digit2 = (value / 100) % 10;
			int digit3 = (value / 1000) % 10;
			return (digit3 << 16) | (digit2 << 8) | (digit1 << 4) | digit0;
		}

		public static bool TryParse(string str, out DisplayPageAddress address)
		{
			address = DisplayPageAddress.Empty;

			if(string.IsNullOrEmpty(str) || str.Length < 3)
				return false;

			int magazineNumber = MinMagazineNumber;
			int pageNumber = 0;
			int subCodeNumber = 0;

			// Only page number
			if(!int.TryParse(str.Substring(0, 1), NumberStyles.HexNumber, null, out magazineNumber) ||
				magazineNumber < MinMagazineNumber || magazineNumber > MaxMagazineNumber)
				return false;
			
			if(!int.TryParse(str.Substring(1, 2), NumberStyles.HexNumber, null, out pageNumber) ||
				pageNumber < 0 || pageNumber > MaxPageNumber)
				return false;

			if(str.Length == 3)
			{
				address = new DisplayPageAddress(magazineNumber, pageNumber, 0);
				return true;
			}

			if(str.Length > 4)
			{
				if(!int.TryParse(str.Substring(4), NumberStyles.HexNumber, null, out subCodeNumber) ||
					subCodeNumber < 0 || subCodeNumber > MaxSubCodeNumber)
					return false;
				
				address = new DisplayPageAddress(magazineNumber, pageNumber, subCodeNumber);
				return true;
			}

			return false;
		}

		#region IComparable Members & Stuff
		public int CompareTo(object obj)
		{
			return this.Value.CompareTo(((DisplayPageAddress)obj).Value);
		}

		public override int GetHashCode()
		{
			return (int)this.Value;
		}

		public override bool Equals(object obj)
		{
			if(obj == null)
				return false;

			if(!(obj is DisplayPageAddress))
				return false;

			return this.CompareTo(obj) == 0;
		}

		public bool Equals(DisplayPageAddress obj)
		{
			if((object)obj == null)
				return false;

			return this.CompareTo(obj) == 0;
		}

		public static bool operator ==(DisplayPageAddress a, DisplayPageAddress b)
		{
			if(object.ReferenceEquals(a, b))
				return true;

			if(((object)a == null) || ((object)b == null))
				return false;

			return a.Equals(b);
		}

		public static bool operator !=(DisplayPageAddress a, DisplayPageAddress b)
		{
			return !(a == b);
		}

		public static bool operator <(DisplayPageAddress a, DisplayPageAddress b)
		{
			return a.CompareTo(b) < 0;
		}

		public static bool operator <=(DisplayPageAddress a, DisplayPageAddress b)
		{
			return a.CompareTo(b) <= 0;
		}

		public static bool operator >(DisplayPageAddress a, DisplayPageAddress b)
		{
			return a.CompareTo(b) > 0;
		}

		public static bool operator >=(DisplayPageAddress a, DisplayPageAddress b)
		{
			return a.CompareTo(b) >= 0;
		}
		#endregion IComparable Members & Stuff
		
		#region IComparable<DisplayPageAddress> Members
		public int CompareTo(DisplayPageAddress other)
		{
			return this.CompareTo((object)other);
		}
		#endregion

		#region IEquatable<DisplayPageAddress> Members
		bool IEquatable<DisplayPageAddress>.Equals(DisplayPageAddress other)
		{
			return this.Equals(other);
		}
		#endregion
	}
}
